import { stringUtils } from 'my-ts-core';
import type { StringKeyObject } from 'my-ts-core/types';

import { RouteMapService } from './route-map-service';
import type { User } from './user';

/**
 * IMPORTANT!!!
 * Always keep it in mind that permission / authentication / authorization MUST be applied on server side first.
 * Any client side check is for user experience ONLY.
 * NEVER TRUST CLIENT!
 */
export class RouteMapManager {
  static readonly routeSingleWildcard: string = '/*';
  static readonly routeDoubleWildcard: string = '/**';
  static readonly denyAll: string = 'denyAll';
  static readonly permitAll: string = 'permitAll';
  static readonly isAuthenticatedPrefix: string = 'isAuthenticated';
  static readonly isAuthenticated: string = `${this.isAuthenticatedPrefix}()`;
  static readonly hasRolePrefix: string = 'hasRole';
  static readonly hasAnyRolePrefix: string = 'hasAnyRole';
  static readonly hasNoRolePrefix: string = 'hasNoRole';
  static readonly hasNoAnyRolePrefix: string = 'hasNoAnyRole';

  /**
   * Returns the expression like hasRole('ROLE_ADMIN').
   * [roleAuthority] is the string of role authority. AKA role name.
   */
  static hasRole(roleAuthority: string): string {
    return this.buildExpressionWithRole(this.hasRolePrefix, roleAuthority);
  }

  /**
   * Returns the expression like hasAnyRole('ROLE_USER','ROLE_ADMIN').
   * [roleAuthorities] is the list string of role authority. AKA role name.
   */
  static hasAnyRole(roleAuthorities: string[]): string {
    return this.buildExpressionWithRoles(this.hasAnyRolePrefix, roleAuthorities);
  }

  /**
   * Returns the expression like hasNoRole('ROLE_ADMIN').
   * [roleAuthority] is the string of role authority. AKA role name.
   */
  static hasNoRole(roleAuthority: string): string {
    return this.buildExpressionWithRole(this.hasNoAnyRolePrefix, roleAuthority);
  }

  /**
   * Returns the expression like hasNoAnyRole('ROLE_USER','ROLE_ADMIN').
   * [roleAuthorities] is the list string of role authority. AKA role name.
   */
  static hasNoAnyRole(roleAuthorities: string[]): string {
    return this.buildExpressionWithRoles(this.hasNoAnyRolePrefix, roleAuthorities);
  }

  /**
   * Checks requested url access permission on security configAttribute.
   */
  static checkRouteOnConfigAttribute(user: User | null, configAttribute: string): boolean {
    let granted = false;

    if (stringUtils.isNullOrUndefinedOrEmptyString(configAttribute)) {
      return false;
    }

    if (configAttribute.startsWith(this.denyAll)) {
      granted = false;
    } else if (configAttribute.startsWith(this.permitAll)) {
      granted = true;
    } else if (configAttribute.startsWith(this.isAuthenticatedPrefix) && user != null) {
      granted = true;
    } else if (
      (configAttribute.startsWith(this.hasRolePrefix) ||
        configAttribute.startsWith(this.hasAnyRolePrefix)) &&
      user != null
    ) {
      granted = RouteMapService.containsAnyRoleInConfigAttribute(
        user!.fullRoles!.map((e) => e.authority!)!,
        configAttribute,
      );
    } else if (
      (configAttribute.startsWith(this.hasNoRolePrefix) ||
        configAttribute.startsWith(this.hasNoAnyRolePrefix)) &&
      user != null
    ) {
      granted = !RouteMapService.containsAnyRoleInConfigAttribute(
        user!.fullRoles!.map((e) => e.authority!)!,
        configAttribute,
      );
    }

    return granted;
  }

  /**
   * Checks if the provided route can be activated.
   * The route can be activated only when it is open route or current user is authenticated.
   * If cannot activate the route then will be redirected to the [Login].
   */
  static checkCanActivateRoute(
    routeMapConfig: StringKeyObject,
    routeName: string,
    user: User | null,
  ): boolean {
    // Gets route map config by route name.
    const routeMapping = routeMapConfig;
    let routeConfig: string;
    let tempRoute: string;
    let canActivate = false;

    for (const key in routeMapping) {
      // First of all we need to remove trailing wildcard.
      // For example, item.Url is /user/** or /user/*.
      // routeConfig is /user.
      routeConfig = key.replace('/**', '');
      routeConfig = routeConfig.replace('/*', '');

      // Removes the routeConfig from routeName.
      // Before removing, /user/get.
      // After removing, /get.
      tempRoute = routeName.replace(routeConfig, '');

      if (stringUtils.isNullOrUndefinedOrEmptyString(tempRoute)) {
        // If tempRoute is empty that means the route is exactly matched with routeConfig.
        // Then we need to check on configAttribute.
        canActivate = this.checkRouteOnConfigAttribute(user, routeMapping[key]);

        break;
      }

      if (tempRoute === routeName) {
        // If routeConfig is /user/status then tempRoute will be the same as routeName.
        // That means a mismatch. We need to check on next.
        continue;
      }

      if (!tempRoute.startsWith('/')) {
        // If tempUrl does NOT start with "/" that means the parent url does not fully match.
        // For example
        // configuredUrl: /measuringData
        // urlWithoutRoot: /measuringDataSharing/queryValidDetails
        // tempUrl will be Sharing/queryValidDetails
        continue;
      }

      // Checks if url starts with tempUrl.
      // For example /authentication is configured but NOT /api/authentication.
      // We only check wildcard config if url ends with tempUrl.
      if (!routeName.startsWith(tempRoute)) {
        if (key.endsWith(this.routeDoubleWildcard)) {
          // If tempRoute is /get or /status/check it would also match the double wildcard.
          // Then we need to check on configAttribute.
          canActivate = this.checkRouteOnConfigAttribute(user, routeMapping[key]);
        } else if (key.endsWith(this.routeSingleWildcard)) {
          // If tempRoute is /get it can match the single wildcard. But /status/check does NOT.
          // Then we need to check on configAttribute.
          if (tempRoute.indexOf('/') === tempRoute.lastIndexOf('/')) {
            canActivate = this.checkRouteOnConfigAttribute(user, routeMapping[key]);
          }
        }
      }
    }

    return canActivate;
  }

  /**
   * Returns the expression like hasRole('ROLE_ADMIN').
   * [prefix] is string of expression prefix. Like hasRole, hasNoRole.
   * [roleAuthority] is the string of role authority. AKA role name.
   */
  private static buildExpressionWithRole(prefix: string, roleAuthority: string): string {
    return `${prefix}('${roleAuthority}')`;
  }

  /**
   * Returns the expression like hasAnyRole('ROLE_USER','ROLE_ADMIN').
   * [prefix] is string of expression prefix. Like hasRole, hasNoRole.
   * [roleAuthorities] is the list string of role authority. AKA role name.
   */
  private static buildExpressionWithRoles(prefix: string, roleAuthorities: string[]): string {
    // The initial string of the expression.
    let result = `${prefix}(`;

    // Goes through the list of roleAuthorities and appends it to result.
    for (const p of roleAuthorities) {
      result += `'${p}',`;
    }

    // Removes the last comma and closes with closing bracket.
    result = `${result.substring(0, result.length - 1)})`;

    return result;
  }
}
